/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.datatheorem.ohos.trustkit.config;


import com.datatheorem.ohos.trustkit.utils.TrustKitLog;

import ohos.app.Context;
import ohos.global.resource.NotExistException;


import org.dom4j.Document;
import org.dom4j.Element;

import java.io.IOException;
import java.io.InputStream;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.logging.Logger;


class TrustKitConfigurationParser {

    /**
     * Parse an XML TrustKit / Network Security policy and return the corresponding
     * {@link TrustKitConfiguration}.
     *
     * @param context
     * @param parser
     * @return TrustKitConfiguration
     * @throws IOException
     * @throws CertificateException
     */
    public static TrustKitConfiguration fromXmlPolicy(
        Context context, Document parser
    ) throws IOException, CertificateException {
        // Handle nested domain config tags
        List<DomainPinningPolicy.Builder> builderList = new ArrayList<>();

        DebugOverridesTag debugOverridesTag = null;

        Element root = parser.getRootElement();
        List<Element> els = root.elements();
        if (els != null && els.size() > 0) {
            for (Element e : els) {
                if ("domain-config".equals(e.getName())) {
                    readDomainConfig(e, builderList);
                } else if ("debug-overrides".equals(e.getName())) {
                    debugOverridesTag = readDebugOverrides(context, e);
                }
            }
        }

        // Finally, store the result of the parsed policy in our configuration object
        TrustKitConfiguration config;
        HashSet<DomainPinningPolicy> domainConfigSet = new HashSet<>();
        for (DomainPinningPolicy.Builder builder : builderList) {
            DomainPinningPolicy policy = builder.build(context);
            if (policy != null) {
                domainConfigSet.add(policy);
            }
        }

        if (debugOverridesTag != null) {
            config = new TrustKitConfiguration(
                domainConfigSet,
                debugOverridesTag.overridePins,
                debugOverridesTag.debugCaCertificates
            );
        } else {
            config = new TrustKitConfiguration(domainConfigSet);
        }
        return config;
    }

    // Heavily inspired from
    private static List<DomainPinningPolicy.Builder> readDomainConfig(
        Element parser, List<DomainPinningPolicy.Builder> parentBuilder
    ) throws IOException {
        DomainPinningPolicy.Builder builder = new DomainPinningPolicy.Builder()
            .setParent(null);
        if ("domain-config".equals(parser.getName())) {
            builder.setHostname(parser.element("domain").getStringValue())
                .setShouldIncludeSubdomains(Boolean.getBoolean(parser.element("domain").attributeValue("includeSubdomains")));

            PinSetTag pinSetTag = readPinSet(parser.element("pin-set"));
            builder.setPublicKeyHashes(pinSetTag.pins)
                .setExpirationDate(pinSetTag.expirationDate);

            TrustkitConfigTag trustkitTag = readTrustkitConfig(parser.element("trustkit-config"));
            builder.setReportUris(trustkitTag.reportUris)
                .setShouldEnforcePinning(trustkitTag.enforcePinning)
                .setShouldDisableDefaultReportUri(trustkitTag.disableDefaultReportUri);
        }
        return parentBuilder;
    }

    private static class PinSetTag {
        Date expirationDate = null;
        Set<String> pins = null;
    }

    private static PinSetTag readPinSet(Element parser) {
        PinSetTag pinSetTag = new PinSetTag();
        pinSetTag.pins = new HashSet<>();

        // Look for the expiration attribute
        String expirationDate = parser.attributeValue("expiration");
        if (expirationDate != null) {
            try {
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
                sdf.setLenient(false);
                Date date = sdf.parse(expirationDate);
                if (date == null) {
                    throw new ConfigurationException("Invalid expiration date in pin-set");
                }
                pinSetTag.expirationDate = date;
            } catch (ParseException e) {
                throw new ConfigurationException("Invalid expiration date in pin-set");
            }
        }

        // Parse until the corresponding close pin-set tag
        List<Element> shaElements = parser.elements();

        if (!shaElements.isEmpty()) {
            for (int i = 0; i < shaElements.size(); i++) {
                if (!"SHA-256".equals(shaElements.get(i).getName())) {
                    pinSetTag.pins.add(shaElements.get(i).getStringValue());
                } else {
                    throw new IllegalArgumentException("Unexpected digest value: " + shaElements.get(i).getName());
                }
            }
        }
        return pinSetTag;
    }

    private static class TrustkitConfigTag {
        Boolean enforcePinning = false;
        Boolean disableDefaultReportUri;
        Set<String> reportUris;
    }

    private static TrustkitConfigTag readTrustkitConfig(Element parser)
        throws IOException {

        TrustkitConfigTag result = new TrustkitConfigTag();
        Set<String> reportUris = new HashSet<>();

        // Look for the enforcePinning attribute
        String enforcePinning = parser.attributeValue("enforcePinning");
        if (enforcePinning != null) {
            result.enforcePinning = Boolean.parseBoolean(enforcePinning);
        }

        // Look for the disableDefaultReportUri attribute
        String disableDefaultReportUri = parser.attributeValue("disableDefaultReportUri");
        if (disableDefaultReportUri != null) {
            result.disableDefaultReportUri = Boolean.parseBoolean(disableDefaultReportUri);
        }

        // Parse until the corresponding close trustkit-config tag
        List<Element> mReportUri = parser.elements();
        if (!mReportUri.isEmpty()) {
            for (Element element : mReportUri) {
                if (element.getName() == "report-uri") {
                    reportUris.add(element.getStringValue());
                }
            }
        }
        result.reportUris = reportUris;
        return result;
    }

    private static class DebugOverridesTag {
        boolean overridePins = false;
        Set<Certificate> debugCaCertificates = null;
    }

    private static DebugOverridesTag readDebugOverrides(Context context, Element parser)
        throws CertificateException, IOException {

        DebugOverridesTag result = new DebugOverridesTag();
        Boolean lastOverridePinsEncountered = null;
        Set<Certificate> debugCaCertificates = new HashSet<>();

        if (parser.element("trust-anchors") != null) {
            // Look for the next certificates tag
            List<Element> listAnchors = parser.element("trust-anchors").elements();
            if (!listAnchors.isEmpty()) {
                for (Element element : listAnchors) {
                    // allows setting overridePins for each debug certificate bundles
                    boolean currentOverridePins = Boolean.parseBoolean(element.attributeValue("overridePins"));
                    if ((lastOverridePinsEncountered != null)
                        && (lastOverridePinsEncountered != currentOverridePins)) {
                        lastOverridePinsEncountered = false;
                        TrustKitLog.w("Warning: different values for overridePins are set in the " +
                            "policy but TrustKit only supports one value; using " +
                            "overridePins=false for all " +
                            "connections");
                    } else {
                        lastOverridePinsEncountered = currentOverridePins;
                    }


                    // Parse the supplied certificate file
                    String caPathFromUser = element.attributeValue("src").trim();

                    caPathFromUser = formatCertPathResourceWhenId(context, caPathFromUser);

                    // Parse the path to the certificate bundle for src=@raw - we ignore system or user
                    // as the src
                    if (!caPathFromUser.isEmpty() && !caPathFromUser.equals("user")
                        && !caPathFromUser.equals("system") && caPathFromUser.startsWith("@raw")) {


                        InputStream stream = context.getResourceManager().
                            getRawFileEntry((context.getBundleName() + "raw" + caPathFromUser.split("/")[1]))
                            .openRawFile();

                        debugCaCertificates.add(CertificateFactory.getInstance("X.509")
                            .generateCertificate(stream));

                    } else {
                        TrustKitLog.i("No <debug-overrides> certificates found by TrustKit." +
                            " Please check your @raw folder " +
                            "(TrustKit doesn't support system and user installed certificates).");
                    }
                }
            }
        }

        if (lastOverridePinsEncountered != null) {
            result.overridePins = lastOverridePinsEncountered;
        }
        if (debugCaCertificates.size() > 0) {
            result.debugCaCertificates = debugCaCertificates;
        }
        return result;
    }

    private static String formatCertPathResourceWhenId(Context context, String caPathFromUser) {
        try {
            String mResource = "@" + context.getResourceManager()
                .getResource(Integer.parseInt(caPathFromUser.replace("@", "")));
            caPathFromUser = mResource.replace(context.getBundleName() + ":", "");
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NotExistException e) {
            e.printStackTrace();
        }
        return caPathFromUser;
    }

    public static boolean isDigitsOnly(CharSequence str) {
        final int len = str.length();
        for (int cp, i = 0; i < len; i += Character.charCount(cp)) {
            cp = Character.codePointAt(str, i);
            if (!Character.isDigit(cp)) {
                return false;
            }
        }
        return true;
    }
}
